package com.coincalf.wallet.service.impl;

import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import com.coincalf.framework.enums.RechargeStatus;
import com.coincalf.framework.enums.TxStateEnum;
import com.coincalf.framework.utils.AddressUtil;
import com.coincalf.wallet.blockchain.*;
import com.coincalf.wallet.blockchain.bean.TransInfo;
import com.coincalf.wallet.blockchain.util.AESUtil;
import com.coincalf.wallet.entity.CoinRecharge;
import com.coincalf.wallet.entity.CollectCoinRecord;
import com.coincalf.wallet.entity.UserAddress;
import com.coincalf.wallet.mapper.CollectCoinRecordMapper;
import com.coincalf.wallet.service.CoinRechargeService;
import com.coincalf.wallet.service.CollectCoinRecordService;
import com.coincalf.wallet.service.UserAddressService;
import com.coincalf.wallet.task.withdraw.CoinWithdrawTask;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.web3j.utils.Convert;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;

@Slf4j
@Service
@Transactional(rollbackFor = Exception.class)
public class CollectCoinRecordServiceImpl extends ServiceImpl<CollectCoinRecordMapper, CollectCoinRecord> implements CollectCoinRecordService {


    @Autowired
    private UserAddressService userAddressService;
    @Autowired
    private CoinWithdrawTask coinWithdrawTask;
    @Autowired
    private CoinRechargeService coinRechargeService;
    @Autowired
    private CollectCoinRecordMapper collectCoinRecordMapper;

    /**
     * 各币种归集地址
     * 说明：USDT_ERC20转账需要ETH作为手续费，USDT_OMNI转账需要BTC作为手续费
     *
     * @param c
     * @return
     */
    @Value("${blockchain.btc.collect.address}")
    public String btcCollectAddress;//btc归集收款地址
    @Value("${blockchain.eth.collect.address}")
    public String ethCollectAddress;//eth归集收款地址
    @Value("${blockchain.usdt.omni.collect.address}")
    public String usdtOmniCollectAddress;//USDT_OMNI归集收款地址
    @Value("${blockchain.usdt.erc20.collect.address}")
    public String usdtErc20CollectAddress;//USDT_ERC20归集收款地址

    //归集手续费地址,即提供手续费的地址 ETH
    @Value("${blockchain.eth.fee.address}")
    public String ethCollectFeeAddress;//ETH手续费地址
    @Value("${blockchain.eth.fee.address.prk}")
    public String ethCollectFeeAddressPrk;//ETH手续费地址私钥

    //BTC手续费地址
    @Value("${blockchain.btc.fee.address}")
    public String btcCollectFeeAddress;
    //BTC手续费地址私钥
    @Value("${blockchain.btc.fee.address.prk}")
    public String btcCollectFeeAddressPrk;

    @Value("${eth.tx.min.fee}")
    public BigDecimal ethMInFee;//ETH归集USDT所需转账手续费

    //USDT最小归集数量
    private static final BigDecimal USDT_MIN_COLLECT_AMOUNT = BigDecimal.valueOf(10L);


    @Override
    public String collectEth(CoinRecharge c) {
        String txId = "";
        //地址校验
        String toAddress = this.ethCollectAddress;
        if (!AddressUtil.verifyEthAddress(toAddress)) {
            log.error("请设置正确的ETH归集收款地址格式 address={}", toAddress);
            return txId;
        }
        //金额校验
        if (c.getMum().compareTo(new BigDecimal("0.01")) < 0) {
            log.error("ETH数量太低 不执行归集操作 address={} amount={}", toAddress, c.getMum());
            return txId;
        }
        UserAddress userAddress = userAddressService.queryByUserAndCoin(c.getUserId(), c.getCoinId());
        if (null == userAddress) {
            log.warn("本次归集用户不存在，请检查用户充币地址表user_address userId={}", c.getUserId());
            return null;
        }
        TransInfo transInfo = new TransInfo();
        transInfo.setFromAddress(c.getAddress());
        transInfo.setToAddress(toAddress);
        //不能归集全部余额 要留出手续费
        BigDecimal gasPrice = coinWithdrawTask.getGasPriceByApi();
        BigDecimal feeEth = Convert.fromWei(gasPrice, Convert.Unit.GWEI).multiply(BigDecimal.valueOf(21000));
        BigDecimal amount = c.getMum().subtract(feeEth);
        transInfo.setAmount(amount);
        transInfo.setFee(gasPrice);
        transInfo.setPrivateKey(userAddress.getPrivateKey());
        TransInfo trans = EthService.trans(transInfo);
        if (trans.getStatus().equals(TxStateEnum.TO_BE_CONFIRMED.getStatus())) {
            txId = trans.getTxHash();
            //归集记录写入数据库 同时充币记录 是否归集修改为1 代表已经归集
            CollectCoinRecord collect = new CollectCoinRecord();
            collect.setCoinId(c.getCoinId());
            collect.setCoinName(c.getCoinName());
            collect.setCoinRechargeId(c.getId().toString());
            collect.setTxStatus(TxStateEnum.TO_BE_CONFIRMED.getStatus());
            collect.setFromAddress(c.getAddress());
            collect.setUserId(c.getUserId().toString());
            collect.setCollectAmount(amount);
            collect.setToAddress(toAddress);
            BigDecimal fee = Convert.fromWei(gasPrice, Convert.Unit.GWEI).multiply(BigDecimal.valueOf(21000));
            collect.setFee(fee);
            collect.setTxId(txId);
            collect.setCreated(new Date());
            collect.setLastUpdateTime(new Date());
            //归集记录写入数据库
            super.insert(collect);
            //更新充币表的归集状态为已归集  如果交易失败 再修改为未归集
            c.setLastUpdateTime(new Date());
            c.setCollected(1);
            coinRechargeService.updateById(c);
        } else {
            log.error("ETH归集交易失败 from={} uid={} amount={}", c.getAddress(), c.getUserId(), amount);
        }
        return txId;
    }

    @Override
    public String collectBtc(CoinRecharge c) {
        String txId = "";
        String toAddress = this.btcCollectAddress;
        //金额校验
        if (c.getMum().compareTo(new BigDecimal("0.001")) < 0) {
            log.error("BTC数量太低 不执行归集操作 address={} amount={}", toAddress, c.getMum());
            return txId;
        }
        UserAddress userAddress = userAddressService.queryByUserAndCoin(c.getUserId(), c.getCoinId());
        if (null == userAddress) {
            log.warn("本次归集用户不存在，请检查用户充币地址表user_address userId={}", c.getUserId());
            return null;
        }

        String privateKey = userAddress.getPrivateKey();
        privateKey = AESUtil.decrypt(privateKey, BlockConstant.AES_KEY);
        BigDecimal btcFeeRate = coinWithdrawTask.getBtcFeeRate();
        long value = c.getMum().multiply(BlockConstant.SATOSHIS).longValue();
        long fee = (2 * 148 + 34 * 2 + 10) * btcFeeRate.longValue();
        txId = BtcService.offlineSignature(c.getAddress(), toAddress, privateKey, value - fee, fee);
        if (StringUtils.isNotBlank(txId)) {
            //归集记录写入数据库 同时充币记录 是否归集修改为1 代表已经归集
            CollectCoinRecord collect = new CollectCoinRecord();
            collect.setCoinId(c.getCoinId());
            collect.setCoinRechargeId(c.getId().toString());
            collect.setTxStatus(TxStateEnum.TO_BE_CONFIRMED.getStatus());
            collect.setFromAddress(c.getAddress());
            collect.setUserId(c.getUserId().toString());
            collect.setCollectAmount(c.getMum());
            collect.setToAddress(toAddress);
            collect.setFee(BigDecimal.valueOf(fee).divide(BlockConstant.SATOSHIS, 6, RoundingMode.DOWN));
            collect.setTxId(txId);
            collect.setCreated(new Date());
            collect.setLastUpdateTime(new Date());
            //归集记录写入数据库
            super.insert(collect);
            //更新充币表的归集状态为已归集  如果交易失败 再修改为未归集
            c.setLastUpdateTime(new Date());
            c.setCollected(1);
            coinRechargeService.updateById(c);
        } else {
            log.error("BTC归集交易失败 from={} uid={} amount={}", c.getAddress(), c.getUserId(), c.getMum());
        }
        return txId;
    }

    @Override
    public void collectUsdtErc20(List<CoinRecharge> coinRecharges) {
        log.info("本次USDT_ERC20待归集数据数量={}", coinRecharges.size());
        //userAddresses用户保存已经转账ETH的用户地址
        Map<String, Long> userAddresses = new HashMap<>(32);
        for (CoinRecharge c : coinRecharges) {
            //归集之前操作
            if (userAddresses.containsKey(c.getAddress())) {
                continue;
            }
            //满足条件则进行归集操作：地址余额(充币数量)大于一定数量
            log.info("即将进行USDT_ERC20归集 address={} uid={}", c.getAddress(), c.getUserId());
            List<CoinRecharge> cos = coinRechargeService.findAllByCoinNameAndTxStatusAndCollectedAndToAddress(
                    "USDT_ERC20",
                    RechargeStatus.SUCCESS.getCode(),
                    0,
                    c.getAddress().toLowerCase()
            );
            if (cos.size() < 1) {
                continue;
            }
            //地址判断
            if (c.getAddress().equalsIgnoreCase(this.usdtErc20CollectAddress) || c.getAddress().equalsIgnoreCase(this.ethCollectFeeAddress)) {
                log.warn("USDT_ERC20->归集地址OR手续费地址不执行归集操作 address={}", c.getAddress());
                //设置归集状态为不需要归集
                coinRechargeService.updateTxStatus(c.getId(), 2);
                continue;
            }

            BigDecimal balance = Erc20TokenService.getTokenService(BlockConstant.USDT_ERC20_ADDRESS).getBalance(c.getAddress());
            if (balance == null) {
                log.info("USDT_ERC20余额查询失败 address={}", c.getAddress());
                continue;
            }
            if (balance.compareTo(CollectCoinRecordServiceImpl.USDT_MIN_COLLECT_AMOUNT) < 0) {
                log.warn("用户地址USDT_ERC20余额未达到最小归集金额，不做处理... uid={} address={}", c.getUserId(), c.getAddress());
                continue;
            }

            //归集之前查询转账地址余额，如果不够 则加入到 待转ETH作为手续费然后归集地址表
            if (!userAddresses.containsKey(c.getAddress())) {
                boolean result = this.queryEthBalance(c.getAddress());
                userAddresses.put(c.getAddress(), c.getUserId());
                if (!result) {
                    log.warn("该用户地址ETH余额不足以进行USDT归集操作 暂且跳过  uid={} address={}",
                            c.getUserId(), c.getAddress()
                    );
                    continue;
                }
            } else {
                continue;
            }
            //将要执行归集操作
            this.usdtErc20Collect(cos, balance);
        }
    }

    @Override
    public String collectUsdtOmni(CoinRecharge c) {
        String txId = "";
        //USDT_OMNI归集收款地址
        String toAddress = this.usdtOmniCollectAddress;
        UserAddress userAddress = userAddressService.queryByUserAndCoin(c.getUserId(), c.getCoinId());
        if (null == userAddress) {
            log.warn("用户不存在，请检查用户地址表user_address userId={}", c.getUserId());
            return null;
        }

        String privateKey = userAddress.getPrivateKey();
        privateKey = AESUtil.decrypt(privateKey, BlockConstant.AES_KEY);
        /**
         * BTC转账费率计算 2 * 148表示两个交易输入 4 * 34表示4个交易输出
         * 这里直接写死，实际情况应该根据具体的交易输入以及交易输出个数来决定
         */
        long fee = (2 * 148 + 34 * 4 + 10) * coinWithdrawTask.getBtcFeeRate().longValue();
        try {
            //USDT_OMNI归集，指定地址支付手续费
            txId = UsdtService.offlineSignature(
                    c.getAddress(),
                    privateKey,
                    toAddress,
                    btcCollectFeeAddress,
                    AESUtil.decrypt(btcCollectFeeAddressPrk, BlockConstant.AES_KEY),
                    c.getMum().multiply(BlockConstant.SATOSHIS).longValue(),
                    fee
            );

            //归集记录写入数据库 同时充币记录 是否归集修改为2 代表归集中
            CollectCoinRecord collect = new CollectCoinRecord();
            collect.setCoinId(c.getCoinId());
            collect.setCoinRechargeId(c.getId().toString());
            collect.setTxStatus(TxStateEnum.TO_BE_CONFIRMED.getStatus());
            collect.setFromAddress(c.getAddress());
            collect.setUserId(c.getUserId().toString());
            collect.setCollectAmount(c.getMum());
            collect.setToAddress(toAddress);
            collect.setFee(BigDecimal.valueOf(fee).divide(BlockConstant.SATOSHIS, 6, RoundingMode.DOWN));
            collect.setTxId(txId);
            collect.setCreated(new Date());
            collect.setLastUpdateTime(new Date());
            //归集记录写入数据库  同时 更新充币表的归集状态
            super.insert(collect);
            //充币纪录归集状态改为已归集 如果交易失败 再改成未归集
            c.setCollected(1);
            c.setLastUpdateTime(new Date());
            coinRechargeService.updateById(c);
        } catch (BlockException e) {
            log.error("USDT_OMNI归集交易失败 from={} uid={} e={}", c.getAddress(), c.getUserId(), e);
        }
        return txId;
    }

    @Override
    public List<CollectCoinRecord> findAllByCoinNameAndTxStatus(String coinName, Integer txStatus) {
        EntityWrapper<CollectCoinRecord> wrap = new EntityWrapper<>();
        wrap.eq("coin_name", coinName).eq("tx_status", txStatus);
        return this.selectList(wrap);
    }

    @Override
    public int updateTxStatus(Long id, Integer txStatus) {
        return collectCoinRecordMapper.updateTxStatus(id, txStatus);
    }

    /**
     * 获取用户归集转账地址ETH余额
     *
     * @param address
     * @return
     */
    public boolean queryEthBalance(String address) {
        BigDecimal balance = EthService.getEthBalance(address);
        if (balance != null) {
            BigDecimal minEthFee = this.ethMInFee;
            if (balance.compareTo(minEthFee) >= 0) {
                return true;
            }
            try {
                //执行转入手续费操作
                log.info("即将执行转入手续费进行USDT_ERC20归集操作 toAddress={}  amount={}", address, minEthFee);
                TransInfo tx = new TransInfo();
                //提币地址就是提供手续费地址
                tx.setFromAddress(this.ethCollectFeeAddress);
                //手续费地址私钥...
                tx.setPrivateKey(this.ethCollectFeeAddressPrk);
                tx.setToAddress(address);
                tx.setAmount(minEthFee);
                tx.setFee(coinWithdrawTask.getGasPriceByApi());
                //转入手续费
                EthService.trans(tx);
            } catch (Exception e) {
                log.error("USDT_ERC20归集交易转入ETH手续费交易失败 address={} e={}", address, e);
                return false;
            }
        }
        return false;
    }

    /**
     * 执行用户USDT_ERC20归集操作
     * 如果用户多次充币，则一次性全部归集
     * 用户充币纪录的是否归集状态修改为归集中，待归集交易成功之后，再修改为已经归集，即1
     *
     * @param cos
     * @param amount
     */
    public void usdtErc20Collect(List<CoinRecharge> cos, BigDecimal amount) {
        CoinRecharge coinRecharge = cos.get(0);
        Long coinId = coinRecharge.getCoinId();
        String chainName = coinRecharge.getCoinName();
        String fromAddress = coinRecharge.getAddress();
        Long userId = coinRecharge.getUserId();

        TransInfo transInfo = new TransInfo();
        transInfo.setFromAddress(fromAddress);
        //获取转账地址私钥
        UserAddress userAddress = userAddressService.queryByUserAndCoin(userId, coinId);
        if (userAddress == null) {
            log.warn("归集用户资产地址不存在 userId={} coinName={}", userId, chainName);
            return;
        }
        if (StringUtils.isBlank(userAddress.getPrivateKey())) {
            log.warn("归集用户资产地址私钥不存在 userId={} coinName={}", userId, chainName);
            return;
        }
        transInfo.setPrivateKey(userAddress.getPrivateKey());
        String toAddress = this.usdtErc20CollectAddress;
        if (!AddressUtil.verifyEthAddress(toAddress)) {
            log.error("请设置正确的归集地址格式 address={}", toAddress);
            return;
        }
        transInfo.setToAddress(toAddress);
        BigDecimal gWei = coinWithdrawTask.getGasPriceByApi();
        transInfo.setFee(gWei);
        //执行归集链上转账
        TransInfo trans;
        transInfo.setAmount(amount);
        trans = Erc20TokenService.getTokenService(BlockConstant.USDT_ERC20_ADDRESS)
                .trans(transInfo);
        if (trans.getStatus().equals(TxStateEnum.TO_BE_CONFIRMED.getStatus())) {
            log.info("用户{}归集交易广播成功 userId={} fromAddress={} amount={} txId={}",
                    chainName, userId, fromAddress, amount, trans.getTxHash()
            );
            //将用户充币纪录的归集状态修改为已归集
            StringBuilder sb = new StringBuilder();
            for (CoinRecharge c : cos) {
                c.setCollected(1);
                c.setLastUpdateTime(new Date());
                coinRechargeService.updateById(c);
                sb.append(c.getId()).append(",");
            }
            // 归集纪录写入数据库
            this.addRecord(sb.toString(), String.valueOf(userId), fromAddress, toAddress, coinId, amount, trans.getTxHash(), Convert.fromWei(gWei, Convert.Unit.GWEI).multiply(BigDecimal.valueOf(60000)));
            //避免归集瞬间ETH节点调用压力过大 休息一下下
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            log.info("用户{}归集交易失败 userId={} fromAddress={} amount={} txId={}",
                    chainName, userId, fromAddress, amount, trans.getTxHash()
            );
        }
    }

    public void addRecord(String coinRechargeId, String userId, String fromAddress, String toAddress, Long coinId, BigDecimal amount, String txHash, BigDecimal fee) {
        log.info("归集纪录即将写入数据库 归集交易txHash={}", txHash);
        //归集纪录写入
        CollectCoinRecord collection = new CollectCoinRecord();
        collection.setCoinRechargeId(coinRechargeId);
        collection.setUserId(userId);
        collection.setFromAddress(fromAddress);
        collection.setToAddress(toAddress);
        collection.setCoinId(coinId);
        collection.setCollectAmount(amount);
        collection.setFee(fee);
        collection.setTxId(txHash);
        collection.setTxStatus(TxStateEnum.TO_BE_CONFIRMED.getStatus());
        collection.setLastUpdateTime(new Date());
        collection.setCreated(new Date());
        // 归集纪录写入数据库
        super.insert(collection);
        log.info("币种id：{}归集交易纪录成功写入数据库 data={}", coinId, collection.toString());
    }

}
